/*
 * Copyright (c) 2017, www.zm0618.com All Rights Reserved..
 */
package com.light.hessian.exporter.registry;

import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.Response;
import com.ecwid.consul.v1.agent.model.NewService;
import com.light.hessian.consul.util.ConsulClientUtils;
import com.light.hessian.exporter.annotation.HessianService;
import com.light.hessian.exporter.annotation.NullService;
import com.light.hessian.exporter.registry.config.*;
import com.light.hessian.md5.util.MD5Util;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.cglib.proxy.InvocationHandler;
import org.springframework.cglib.proxy.Proxy;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.remoting.caucho.HessianServiceExporter;
import org.springframework.util.ClassUtils;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.*;

/**
 * Created by zhaojun on 2017/02/08.
 * 自动注册Hessian对外服务访问路径  简化hessian服务开发
 */
public class HessianExporterAutoRegister implements ApplicationContextAware,InitializingBean,BeanNameAware,ServiceRegistry,EnvironmentAware {


    protected static final Logger logger= LoggerFactory.getLogger(HessianExporterAutoRegister.class);

    private ApplicationContext applicationContext;

    private String nameSpace;
    private RegistryType registryType;
    private String config;
    private Properties configProperties;


    private String beanName;

    private Environment env;


    @Override
    public void setBeanName(String name) {
        this.beanName=name;
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext=applicationContext;
    }

    @Override
    public void afterPropertiesSet() throws Exception {

        logger.debug("========>>START TO EXPOSE SERVICE ENDPOINTS!<<=========");
        HessianServiceExportHelper exportHelper=new HessianServiceExportHelper();
        exportHelper.setConfigurableAct((ConfigurableApplicationContext) this.applicationContext);
        exportHelper.doExport();
        logger.debug("========>>SERVICE ENDPOINTS HAVE BEEN EXPOSED!<<=======");

        logger.debug("========>>START TO REGISTER SERVICE!<<=========");
        registryService();
        logger.debug("========>>REGISTERING SERVICE IS FINISHED!<<===");
    }

    private RegistryType determineRegistryType() {
        if (configProperties!=null){
            String registryTypeVal=configProperties.getProperty(String.format("%s.service.registry.type",this.getNameSpace()));
            if (registryTypeVal!=null){
                return RegistryType.valueOf(registryTypeVal);
            }else{
                this.registryType=RegistryType.local;
            }
        }
        return this.registryType;
    }


    private void registryService() throws IOException {

        //
        if (this.nameSpace==null){
            this.nameSpace=env.getProperty("spring.application.name");
        }

        if (StringUtils.isNotBlank(config)){
            ClassPathResource registryConfResource=new ClassPathResource(config,ClassUtils.getDefaultClassLoader());
            configProperties = PropertiesLoaderUtils.loadProperties(registryConfResource);
        }
        ServiceRegistryConfig registryConfig = getServiceRegistryConfig();
        if (registryConfig.getRegistryType()== RegistryType.consul){
            ConsulRegistryHelper  registryHelper=new ConsulRegistryHelper((ConsulRegistryExportConfig) registryConfig,this.nameSpace,this.env);
            logger.debug(String.format("========>>REGISTER SERVICE[%s] TO  CONSUL!<<===",this.nameSpace));
            registryHelper.doRegisty();
        }else{
            logger.debug(String.format("========>>REGISTER SERVICE[%s] TO  LOCAL!<<===",this.nameSpace));
        }
    }

    private ServiceRegistryConfig getServiceRegistryConfig() {
        registryType=determineRegistryType();
        ServiceRegistryConfigParser registryConfigParser=null;
        if (registryType==RegistryType.consul){
            registryConfigParser = new ConsulRegistryConfigParser();
        }
        if (registryType== RegistryType.local){
            registryConfigParser = new LocalRegistryConfigParser();
        }

        return registryConfigParser.doParse(this,this.env);
    }

    @Override
    public void setEnvironment(Environment env) {
        this.env =env;
    }


    class ConsulRegistryHelper{

        private ConsulRegistryExportConfig config;
        private String nameSpace;
        private ConsulClient consulClient;


        public ConsulRegistryHelper(ConsulRegistryExportConfig config, String nameSpace, Environment environment) {
            this.config = config;
            this.nameSpace=nameSpace;
            this.consulClient=createConsulClient(config);
        }

        public void doRegisty(){

            NewService newService=new NewService();


            String serviceId=createServiceId(config);
            newService.setId(serviceId);
            newService.setName(this.nameSpace);
            newService.setAddress(config.getAddress());
            newService.setPort(config.getPort());
            newService.setEnableTagOverride(false);
            List<String> tags=new ArrayList<String>();
            tags.add(config.getProtocol());
            tags.add(config.getPrefixPath());
            newService.setTags(tags);

            String healthCheck=config.getHealthCheck();
            if (StringUtils.isNotBlank(healthCheck)){
                NewService.Check check=new NewService.Check();
                check.setHttp(config.getHealthCheck());//目前只支持http协议
                check.setTimeout(config.getHealthCheckTimeout());
                check.setInterval(config.getHealthCheckInterval());
                newService.setCheck(check);
            }

            Response<Void> resp1 = this.consulClient.agentServiceRegister(newService);
            logger.debug("Registry service [{}] ==>{} --",this.nameSpace,resp1);
            Response<Boolean> resp0 = this.consulClient.setKVValue(String.format("%s.%s.cfg.attach",this.nameSpace, serviceId), config.getPrefixPath() + "@" + config.getProtocol());
            logger.debug("Registry service config [{}] ==>{} --",this.nameSpace,resp0);

        }

        private String createServiceId(ConsulRegistryExportConfig config) {//生成注册ID
            String namespace = getNameSpace();
            String address=config.getAddress();
            int port = config.getPort();
            return MD5Util.digest(StringUtils.join(new String[]{namespace,address,String.valueOf(port)},":"));
        }

        private ConsulClient createConsulClient(ConsulRegistryExportConfig config) {


            if (this.consulClient==null){
                String registryNode = selectNode(config);
                this.consulClient= ConsulClientUtils.createClient(registryNode);
            }
            return consulClient;
        }

        public String selectNode(ConsulRegistryExportConfig config) {
            List<String> nodes = config.getRegistryNodes();
            if (CollectionUtils.isNotEmpty(nodes)) {
                String providerNode = nodes.get(0);
                return providerNode;
            }
            return null;
        }
    }




    class HessianServiceExportHelper {

        ConfigurableApplicationContext configurableAct;


        public void setConfigurableAct(ConfigurableApplicationContext configurableAct) {
            this.configurableAct = configurableAct;
        }

        public void doExport(){
            ConfigurableApplicationContext configurableAct= this.configurableAct;
            Map<String, Object> hessianBeanMap = configurableAct.getBeansWithAnnotation(HessianService.class);
            if (hessianBeanMap!=null&&hessianBeanMap.size()>0){
                ConfigurableListableBeanFactory configurableListableBeanFactory=configurableAct.getBeanFactory();
                Set<Map.Entry<String, Object>> beanEntrySet = hessianBeanMap.entrySet();
                for (Map.Entry<String, Object> beanEntry:beanEntrySet) {

                    Object beanObj = beanEntry.getValue();
                    String beanName=beanEntry.getKey();
                    HessianService  hessianService=configurableAct.findAnnotationOnBean(beanName,HessianService.class);
                    Class serviceClazz = hessianService.service();
                    final Class beanClazz= ClassUtils.getUserClass(beanObj);
                    Class<?> beanInterface;
                    if (serviceClazz== NullService.class){
                        Class<?>[] interfaces =ClassUtils.getAllInterfacesForClass(beanClazz);
                        if (interfaces==null||interfaces.length==0){
                            throw new  RuntimeException(String.format("Service Class[%s] must implement a interface!",beanClazz));
                        }
                        beanInterface = interfaces[0];
                    }else{
                        beanInterface=serviceClazz;
                    }
                    String hessianServiceName=hessianService.name();
                    if (StringUtils.length(hessianServiceName)==0){
                        hessianServiceName= createHessianServiceName(beanInterface);
                    }
                    HessianServiceExporter serviceExporter = createServiceExporter(configurableListableBeanFactory, beanObj, beanInterface, hessianServiceName);
                    registerServiceExporter(configurableListableBeanFactory,serviceExporter,hessianServiceName);
                }
            }
        }

        private void registerServiceExporter(ConfigurableListableBeanFactory configurableListableBeanFactory, HessianServiceExporter serviceExporter,String hessianServiceName) {
            if (serviceExporter!=null){
                configurableListableBeanFactory.registerSingleton(hessianServiceName,serviceExporter);
                if (logger.isDebugEnabled()){
                    logger.debug("register hessian service endpoint===>'{}'",hessianServiceName);
                }
            }
        }

        private HessianServiceExporter createServiceExporter(ConfigurableListableBeanFactory configurableListableBeanFactory, Object beanObj, Class<?> beanInterface, String hessianServiceName) {
            if (!configurableListableBeanFactory.containsBean(hessianServiceName)){
                Object insuredServiceObj = insureServiceObj(beanObj, beanInterface, hessianServiceName);
                HessianServiceExporter hessianServiceExporter=new HessianServiceExporter();
                hessianServiceExporter.setServiceInterface(beanInterface);
                hessianServiceExporter.setService(insuredServiceObj);
                hessianServiceExporter.prepare();
                return hessianServiceExporter;
            }
            return null;
        }

        private Object insureServiceObj(Object beanObj, Class<?> beanInterface, String hessianServiceName) {

            final Class<?> beanClazz = ClassUtils.getUserClass(beanObj);
            Class<?>[] allInterfaces = beanClazz.getInterfaces();

            if (allInterfaces==null||allInterfaces.length==0){
                //类未实现接口，外指定服务接口
                if (beanInterface!=null&&beanInterface!=NullService.class){
                    final Object _beanObj=beanObj;

                    Object proxyBeanObj = Proxy.newProxyInstance(ClassUtils.getDefaultClassLoader(), new Class[]{beanInterface}, new InvocationHandler() {//按配置服务接口生成代理对象
                        @Override
                        public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                            Method method0 = ClassUtils.getMethod(beanClazz, method.getName(), method.getParameterTypes());
                            if (method0!=null){
                                if (method.getName().equals(method0.getName())){
                                    return method0.invoke(_beanObj, objects);
                                }
                            }
                            return null;
                        }
                    });

                    return proxyBeanObj;
                }else{
                    throw new  RuntimeException(String.format("There is no service interface!,serviceName='%s',serviceClazz='%s'",hessianServiceName,beanObj.getClass()));
                }
            }
            if (allInterfaces!=null&& ArrayUtils.contains(allInterfaces,beanInterface)){
                return beanObj;
            }
            throw new RuntimeException(String.format("The class implementation of service ['%s'] is error,and serviceClazz='%s'",hessianServiceName,beanObj.getClass()));
        }

        private String createHessianServiceName(Class<?> beanInterface) {
            String beanInterfaceName=beanInterface.getSimpleName();
            String serviceName1= beanInterfaceName.substring(0,1).toLowerCase()+beanInterfaceName.substring(1);
            return "/"+serviceName1;
        }

    }

    public Properties getConfigProperties() {
        return configProperties;
    }

    public String getNameSpace() {
        return nameSpace;
    }

    public void setNameSpace(String nameSpace) {
        this.nameSpace = nameSpace;
    }

    public RegistryType getRegistryType() {
        return registryType;
    }

    public void setRegistryType(RegistryType registryType) {
        this.registryType = registryType;
    }

    public String getConfig() {
        return config;
    }

    public void setConfig(String config) {
        this.config = config;
    }


}
